﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shoozla.DataModel.Helper;



namespace Shoozla.DataModel
{
    public enum SearchMethod
    { 
        Album,
        Song,
        Artist,
        AlbumAndSong,
        All, //album + song + artist
    }

    /// <summary>
    /// Add-in to search for covers
    /// </summary>
    public abstract class CoverSearcher
    {

        //caches
        public ConcurrentDictionary<string, Uri> AlbumUriCache { get; private set; }
        public ConcurrentDictionary<string, Uri> ArtistUriCache { get; private set; }
        public ConcurrentDictionary<string, Uri> SongUriCache { get; private set; }

        //public information
        public String AddInName { get; protected set; }
        public String AddInDescription { get; protected set; }

        private List<Mp3File> _externalMp3List;
        public ICoverStorage CoverImageCache { get; private set; }
        private ParallelOptions _paralleSettings;
        public CancellationToken CancelToken {
            get { return _paralleSettings.CancellationToken;  }
            set { _paralleSettings.CancellationToken = value; }
        }

        //events
        public event EventHandler SearchStarted;
        public event EventHandler SearchCompleted;
        public event CoverFoundHandler CoverFound;
        public delegate void CoverFoundHandler (string displayName, SearchMethod method, Uri urlFound);

        public CoverSearcher()
        {
            SetAddInInformation();

            AlbumUriCache = new ConcurrentDictionary<string, Uri>();
            ArtistUriCache = new ConcurrentDictionary<string, Uri>();
            SongUriCache = new ConcurrentDictionary<string, Uri>();
            SearchCompleted += new EventHandler(CoverSearcher_SearchCompleted);
            _paralleSettings = new ParallelOptions();
        }

        public CoverSearcher(ICoverStorage coverCache, CancellationToken cancelToken)
            : this()
        {
            CoverImageCache = coverCache;
            CancelToken = cancelToken;
        }

        public override string ToString()
        {
            return AddInName;
        }

        protected virtual void SetAddInInformation()
        {
            AddInName = "Generic Cover Searcher";
            AddInDescription = "Add-in Description";
        }

        public virtual Image GetIcon()
        {
            return null;
        }

        /// <summary>
        /// To call before to search
        /// </summary>
        /// <returns>false if the initialization fails</returns>
        public virtual bool Initialize() { return true; }

        public void StopSearch()
        {
            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) =>
            {
                return
                    !String.IsNullOrEmpty(m.AlbumName) &&
                    m.ImageUrl == null;
            }))
            {
                string indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (AlbumUriCache.ContainsKey(indexName))
                {
                    Uri coverUri = AlbumUriCache[indexName];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                String indexKey = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                if (SongUriCache.ContainsKey(indexKey))
                {
                    Uri coverUri = SongUriCache[indexKey];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //assign all pending uris 
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                if (m.Artist != null)
                {
                    if (ArtistUriCache.ContainsKey(m.Artist))
                    {
                        Uri coverUri = ArtistUriCache[m.Artist];
                        if (coverUri != null)
                        {
                            m.ImageUrl = coverUri;
                        }
                    }
                }
            }

            if (SearchCompleted != null)
                foreach (Delegate d in SearchCompleted.GetInvocationList())
                    d.DynamicInvoke(new object[] { null, null });
        }

        public void ClearCache()
        {
            AlbumUriCache.Clear();
            SongUriCache.Clear();
            ArtistUriCache.Clear();
        }

        void CoverSearcher_SearchCompleted(object sender, EventArgs e)
        {
            //i need to delete all empty covers, covers not found, to avoid to search for them every run
            Uri dummyUrl;

            //delete albums
            var emptyItem = (from c in AlbumUriCache
                             where c.Value == null
                             select c);
            foreach (var i in emptyItem)
                AlbumUriCache.TryRemove(i.Key, out dummyUrl);

            //delete songs
            emptyItem = (from c in SongUriCache
                             where c.Value == null
                             select c);
            foreach (var i in emptyItem)
                SongUriCache.TryRemove(i.Key, out dummyUrl);

            //delete artist
            emptyItem = (from c in ArtistUriCache
                         where c.Value == null
                         select c);
            foreach (var i in emptyItem)
                ArtistUriCache.TryRemove(i.Key, out dummyUrl);

            int albumFound = AlbumUriCache.Count((v) => { return v.Value != null; });
            int artistFound = ArtistUriCache.Count((v) => { return v.Value != null; });
            int songFound = SongUriCache.Count((v) => { return v.Value != null; });
            int totalCovers = (AlbumUriCache.Count + ArtistUriCache.Count + SongUriCache.Count) ;
            double average = 0.0f;
            if(totalCovers > 0)
                average = (albumFound + artistFound + songFound) * 100 / totalCovers;
            Trace.WriteLine(String.Format("[INFO] Search Terminated: {0}+{1}+{2} out of {3} -> {4}%", albumFound, songFound, artistFound, totalCovers, average));
        }

        /// <summary>
        /// Syncronous method to find a single cover for an Mp3.
        /// It store the cover in a image cache (CoverImageCache)
        /// </summary>
        /// <param name="song"></param>
        /// <returns>The URI of the cover, or null in negative case.</returns>
        public Uri FindSingleCover(Mp3File song)
        {
            if (song == null || song.Artist == null || song.Title == null)
                return null;

            if (!Initialize()) //if needed ask to the user to connect to the website
                return null;

            Uri cover = null;

            if (!String.IsNullOrEmpty(song.AlbumName) && !String.IsNullOrEmpty(song.Artist))
                cover = FindByAlbumName(song);

            if (cover == null && !String.IsNullOrEmpty(song.Title))
                cover = FindBySongTitle(song);

            if (cover == null)
                cover = FindArtist(song);

            if (cover != null)
                CoverImageCache.AddCoverToStorage(cover.AbsoluteUri);

            return cover;
        }

        protected void RaiseCoverFound(string displayName, SearchMethod method, Uri urlFound)
        {
            Trace.WriteLine(String.Format("[COVER] Found {0}: {1} ", method, displayName));
            if (CoverFound != null)
                CoverFound.Invoke(displayName, method, urlFound);
        }

        #region Thread workers
        //Finds all the covers for the album in the list
        private void AlbumCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            String[] names = Helper.StringHelper.GetSeparatedNames(item.Key);
            if (coverUri == null)               
                coverUri = FindByAlbumName(names[0], names[1]);
            else
                RaiseCoverFound(names[0] + " (" + names[1] + ")", SearchMethod.Album, coverUri);

            AlbumUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
            
        }

        private void SongCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            String[] names = Helper.StringHelper.GetSeparatedNames(item.Key);
            if (coverUri == null)
                coverUri = FindBySongTitle(names[0], names[1]);
            else
                RaiseCoverFound(names[0] + " - " + names[1], SearchMethod.Song, coverUri);
            
            SongUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);

        }

        private void ArtistCoverSearcherWorker(KeyValuePair<string, Uri> item)
        {
            Uri coverUri = item.Value; //can be NOT null --> cover already found --> skip it
            if (coverUri == null)
                coverUri = FindArtist(item.Key);
            else
                RaiseCoverFound(item.Key, SearchMethod.Artist, coverUri);

            ArtistUriCache.AddOrUpdate(item.Key, coverUri, (k, t) => coverUri);
            if (coverUri != null)
                CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
        }

        #endregion

        //this function minimize all the trips to the website, looping in the memory lists (that's is faster)
        /// <summary>
        /// Find all the covers for the given Mp3 list, multithread synchronous function.
        /// All the covers found (Uri) are saved in a cache (CoverImageCache) and downloaded immediately
        /// </summary>
        /// <param name="songs">list of Mp3</param>
        /// <param name="threadingFactor">speed of the search/number of thread</param>
        public void FindMultipleCovers(List<Mp3File> songs, ThreadingFactorEnum threadingFactor)
        {
            if (songs == null || songs.Count <= 0)
                return;

            if (SearchStarted != null)
                SearchStarted.Invoke(null, null);

            _externalMp3List = songs;
            _paralleSettings.MaxDegreeOfParallelism = ThreadHelper.CalculateTreads(songs.Count, ThreadingFactorEnum.Maximum);

            foreach (Mp3File m in _externalMp3List)
                m.ImageUrl = null; //needed when i search the second time for the same song

            //analyze all the albums name, create a list "to download", 1 entry for each album --> no duplicates
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return !String.IsNullOrEmpty(m.AlbumName); }))
            {
                //only song with album name
                String indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (!AlbumUriCache.ContainsKey(indexName))
                    AlbumUriCache.AddOrUpdate(indexName, (Uri)null, (k, v) => null);
            }

            Task parallelTask = null;
            parallelTask = Task.Factory.StartNew(() =>
            {
                //var emptyItem = (from c in AlbumUriCache
                //                  where c.Value == null
                //                  select c);

                Parallel.ForEach(AlbumUriCache, _paralleSettings, list => AlbumCoverSearcherWorker(list));
            }, _paralleSettings.CancellationToken);

            parallelTask.Wait(_paralleSettings.CancellationToken);
            AlbumSearchCompleted();
        }

        #region callbacks

        //called when all album have been found
        void AlbumSearchCompleted()
        {
            //assigning the cover URI to each song
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return !String.IsNullOrEmpty(m.AlbumName); }))
            {
                string indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.AlbumName);
                if (AlbumUriCache.ContainsKey(indexName))
                {
                    Uri coverUri = AlbumUriCache[indexName];
                    if (coverUri != null)
                    {
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //now i have all the album found! check which song needs more work
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                //only song without image url
                if (!String.IsNullOrEmpty(m.Artist) && !String.IsNullOrEmpty(m.Title))
                {
                    String indexName = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                    if (!SongUriCache.ContainsKey(indexName))
                        SongUriCache.AddOrUpdate(indexName, (Uri)null, (k, v) => null);
                }
            }

            Task parallelTask = Task.Factory.StartNew(() =>
            {
                //in this way i will not search for the same covers
                //var emptyItem = (from c in SongUriCache
                //                 where c.Value == null
                //                 select c); 

                Parallel.ForEach(SongUriCache, _paralleSettings, list => SongCoverSearcherWorker(list));
            }, _paralleSettings.CancellationToken);

            parallelTask.Wait(_paralleSettings.CancellationToken);
            SongsSearchCompleted();
        }

        //called when all songs titles have been downloaded
        void SongsSearchCompleted()
        {
            //assign the new cover to the related song
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                String indexKey = Helper.StringHelper.ConcatenateNames(m.Artist, m.Title);
                if (SongUriCache.ContainsKey(indexKey))
                {
                    Uri coverUri = SongUriCache[indexKey];
                    if (coverUri != null)
                    {
                       // CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
                        m.ImageUrl = coverUri;
                    }
                }
            }

            //now i have all the album+songs found! check which song needs more work (artist)
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                //only song without image url
                if (!String.IsNullOrEmpty(m.Artist))
                    if (!ArtistUriCache.ContainsKey(m.Artist))
                        ArtistUriCache.AddOrUpdate(m.Artist, (Uri)null, (k, v) => null);
            }

            //continue with searching

            Task parallelTask = Task.Factory.StartNew(() =>
            {
                //var emptyItem = (from c in ArtistUriCache
                //                 where c.Value == null
                //                 select c);

                Parallel.ForEach(ArtistUriCache, _paralleSettings, list => ArtistCoverSearcherWorker(list));
            }, _paralleSettings.CancellationToken);

            parallelTask.Wait(_paralleSettings.CancellationToken);
            ArtistSearchCompleted();

        }

        //called when all artist have been downloaded
        void ArtistSearchCompleted()
        {
            //assign artists
            foreach (Mp3File m in _externalMp3List.FindAll((m) => { return m.ImageUrl == null; }))
            {
                if (m.Artist != null)
                {
                    if (ArtistUriCache.ContainsKey(m.Artist))
                    {
                        Uri coverUri = ArtistUriCache[m.Artist];
                        if (coverUri != null)
                        {
                         //   CoverImageCache.AddCoverToStorage(coverUri.AbsoluteUri);
                            m.ImageUrl = coverUri;
                        }
                    }
                }
            }
           
            //done, all the songs with a cover have a ImageUri value != null --> the image is downloaded in parallel
            if (SearchCompleted != null)
                foreach(Delegate d in SearchCompleted.GetInvocationList())
                    d.DynamicInvoke(new object[]{null,null});
        }

        #endregion

        #region Find methods

        #region Album
        public Uri FindByAlbumName(Mp3File song)
        {
            return FindByAlbumName(song.Artist, song.AlbumName);
        }
        public Uri FindByAlbumName(string artist, string albumName)
        {
            Uri ret = FindByAlbumNameInternal(artist.ToLowerInvariant(), albumName.ToLowerInvariant());
            if (ret != null)
                RaiseCoverFound(albumName + " ("+artist+")", SearchMethod.Album, ret);
            return ret;
        }
        public virtual Uri FindByAlbumNameInternal(String artist, String albumName) { return null; }
        #endregion

        #region Title
        public Uri FindBySongTitle(Mp3File song)
        {
           return FindBySongTitle(song.Artist, song.Title);
        }
        public Uri FindBySongTitle(string artist, string title)
        {
            Uri ret = FindBySongTitleInternal(artist.ToLowerInvariant(), title.ToLowerInvariant());
            if (ret != null)
                RaiseCoverFound(artist + " - " + title, SearchMethod.Song, ret);
            return ret;
        }
        public virtual Uri FindBySongTitleInternal(String artist, String title) { return null; }
        #endregion

        #region Artist
        public Uri FindArtist(Mp3File song)
        {
            return FindArtist(song.Artist.ToLowerInvariant());
        }
        public Uri FindArtist(string artist)
        {
            Uri ret = FindArtistInternal(artist);
            if (ret != null)
                RaiseCoverFound(artist,SearchMethod.Artist, ret);
            return ret;
           
        }
        public virtual Uri FindArtistInternal(String artist) { return null; }
        #endregion

        #endregion

    }
}
